package ar.edu.itba.it.cg.grupo01.impl;

import static com.google.common.base.Preconditions.checkArgument;

import java.util.Iterator;

import javax.vecmath.Point3f;
import javax.vecmath.Vector3f;

import ar.edu.itba.it.cg.grupo01.api.CameraInterface;
import ar.edu.itba.it.cg.grupo01.api.Ray;

public class Camera implements CameraInterface {

	private Point3f origin;
	private Vector3f viewingDirection;
	private Vector3f upVector;
	private int xResolution;
	private int yResolution;
	private float fov; // horizontal fov
	private Vector3f normal;
	private Point3f initialPoint;
	private Iterator<CameraRay> rayIterator;

	public Camera() {
	}

	public Vector3f getViewingDirection() {
		return viewingDirection;
	}

	public Vector3f getUpVector() {
		return upVector;
	}

	public float getFov() {
		return fov;
	}

	public Camera(Point3f origin, Vector3f viewingDirection, Vector3f upVector,
			int resX, int resY, float fov) {

		initializeCameraParameters(origin, viewingDirection, upVector, resX,
				resY, fov);

		// TODO remember jabu
		// El archivo de escena viene mal entonces hay que hacer (up (cross
		// product) viewing)
		// para sacar el normal y despues normal (cross) viewing para tener el
		// up
		// arreglado

		this.normal = new Vector3f();
		normal.cross(upVector, viewingDirection);
		upVector.cross(viewingDirection, normal);
		normal.normalize();
		upVector.normalize();

		// upVector.cross(normal, viewingDirection);

		Point3f focalPoint = (Point3f) origin.clone();

		float distance = calculateFocalPlaneDistance();

		Vector3f p = (Vector3f) viewingDirection.clone();

		p.scale(distance);

		focalPoint.add(p);

		Vector3f u = new Vector3f(normal.x, normal.y, normal.z);
		Vector3f v = new Vector3f(this.upVector.x, this.upVector.y,
				this.upVector.z);

		u.scale(xResolution / 2.0f);
		v.scale(yResolution / 2.0f);

		focalPoint.add(u);
		focalPoint.add(v);

		this.initialPoint = new Point3f(focalPoint.x, focalPoint.y,
				focalPoint.z);

		this.rayIterator = new SingleRay(this);
	}

	@Override
	public int getxResolution() {
		return xResolution;
	}

	@Override
	public int getyResolution() {
		return yResolution;
	}

	@Override
	public Ray getRay(int x, int y) {

		validateRayParameters(x, y);

		Vector3f u = (Vector3f) normal.clone();
		Vector3f v = (Vector3f) upVector.clone();

		// Vectors u & v must be inverted, so, multiply by -1
		scaleVector(v, x * (-1));
		scaleVector(u, y * (-1));

		Vector3f answer = constructResultVector(u, v);

		Ray ray = constructRay(answer);

		return ray;
	}

	@Override
	public Ray getRay(float x, float y) {

		validateRayParameters(x, y);

		Vector3f u = (Vector3f) normal.clone();
		Vector3f v = (Vector3f) upVector.clone();

		// Vectors u & v must be inverted, so, multiply by -1
		scaleVector(v, x * (-1));
		scaleVector(u, y * (-1));

		Vector3f answer = constructResultVector(u, v);

		Ray ray = constructRay(answer);

		return ray;
	}

	@Override
	public Iterator<CameraRay> iterator() {
		return rayIterator;
	}

	@Override
	public void setIterator(Iterator<CameraRay> iterator) {
		this.rayIterator = iterator;
	}

	@Override
	public Point3f getOrigin() {
		return (Point3f) origin.clone();
	}

	// Private Members
	private void initializeCameraParameters(Point3f origin,
			Vector3f viewingDirection, Vector3f upVector, int resX, int resY,
			float fov) {
		this.origin = (Point3f) origin.clone();
		this.viewingDirection = (Vector3f) viewingDirection.clone();
		this.viewingDirection.normalize();
		this.upVector = (Vector3f) upVector.clone();
		this.upVector.normalize();
		this.yResolution = resY;
		this.xResolution = resX;
		// si el fov es el grados, usar fov = fov * 2 * Math.PI / 360;
		this.fov = (float) (fov * 2 * Math.PI / 360);
	}

	// Constructs a Ray from a given direction
	private Ray constructRay(Vector3f answer) {
		Ray ray = new RayImpl(origin, answer);
		return ray;
	}

	// Scales a vector
	private void scaleVector(Vector3f aVector, int scalarValue) {
		aVector.scale(scalarValue);
	}

	// Scales a vector
	private void scaleVector(Vector3f aVector, float scalarValue) {
		aVector.scale(scalarValue);
	}

	// Constructs a resulting vector from the addition of a given pair
	private Vector3f constructResultVector(Vector3f u, Vector3f v) {
		Vector3f answer = new Vector3f(initialPoint.x, initialPoint.y,
				initialPoint.z);
		answer.add(u);
		answer.add(v);
		return answer;
	}

	private void validateRayParameters(int x, int y) {
		checkArgument(x >= 0 && x < yResolution,
				"x should be greater than 0 and lower than " + xResolution
						+ ". Got: " + x);
		checkArgument(y >= 0 && y < xResolution,
				"y should be greater than 0 and lower than " + yResolution
						+ ". Got: " + y);
	}

	private void validateRayParameters(float x, float y) {
		checkArgument(x >= 0 && x < yResolution,
				"x should be greater than 0 and lower than " + xResolution
						+ ". Got: " + x);
		checkArgument(y >= 0 && y < xResolution,
				"y should be greater than 0 and lower than " + yResolution
						+ ". Got: " + y);
	}

	private float calculateFocalPlaneDistance() {
		float distance = (float) ((xResolution / 2.0f) / Math.tan(this.fov / 2));
		return distance;
	}

	public float getFocalPlaneDistance() {
		float distance = (float) ((xResolution / 2.0f) / Math.tan(this.fov / 2));
		return distance;
	}
}
